/*
 * Copyright 2019-2021 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2019-2021 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2019-2021 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses>.
 */
package com.unboundid.ldap.sdk.unboundidds.extensions;



import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import com.unboundid.asn1.ASN1Element;
import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.asn1.ASN1Sequence;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.Debug;
import com.unboundid.util.NotMutable;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.Validator;

import static com.unboundid.ldap.sdk.unboundidds.extensions.ExtOpMessages.*;



/**
 * This class provides an implementation of an extended result that may be used
 * provide the client with the passwords generated by the server in response to
 * a {@link GeneratePasswordExtendedRequest}.
 * <BR>
 * <BLOCKQUOTE>
 *   <B>NOTE:</B>  This class, and other classes within the
 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
 *   supported for use against Ping Identity, UnboundID, and
 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
 *   for proprietary functionality or for external specifications that are not
 *   considered stable or mature enough to be guaranteed to work in an
 *   interoperable way with other types of LDAP servers.
 * </BLOCKQUOTE>
 * <BR>
 * If the extended request was processed successfully, then this result will
 * have an OID of 1.3.6.1.4.1.30221.2.6.63 and a value with the following
 * encoding:
 * <BR><BR>
 * <PRE>
 *   GeneratePasswordResponse ::= SEQUENCE {
 *        passwordPolicyDN       LDAPDN,
 *        generatedPasswords     SEQUENCE OF SEQUENCE {
 *             generatedPassword       OCTET STRING,
 *             validationAttempted     BOOLEAN,
 *             validationErrors        [0] SEQUENCE OF OCTET STRING OPTIONAL,
 *             ... },
 *        ... }
 * </PRE>
 * <BR><BR>
 * The elements of the response value are:
 * <UL>
 *   <LI>passwordPolicyDN -- The DN of the password policy that was used to
 *       select the password generator and validators used in the course of
 *       creating the passwords.</LI>
 *   <LI>generatedPassword -- A clear-text password that was generated by the
 *       server.</LI>
 *   <LI>validationAttempted -- Indicates whether the server attempted to
 *       perform any validation for the generated password.</LI>
 *   <LI>validationErrors -- A list of messages describing any problems
 *       that were identified while validating the generated password.</LI>
 * </UL>
 */
@NotMutable()
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class GeneratePasswordExtendedResult
       extends ExtendedResult
{
  /**
   * The OID (1.3.6.1.4.1.30221.2.6.57) for the generate TOTP shared secret
   * extended result.
   */
  @NotNull public static final String GENERATE_PASSWORD_RESULT_OID =
       "1.3.6.1.4.1.30221.2.6.63";



  /**
   * The serial version UID for this serializable class.
   */
  private static final long  serialVersionUID = -6840636721723079194L;



  // The list of generated passwords returned by the server.
  @NotNull private final List<GeneratedPassword> generatedPasswords;

  // The DN of the password policy that was used in the course of generating
  // the password.
  @Nullable private final String passwordPolicyDN;



  /**
   * Creates a new generate password extended result that indicates successful
   * processing with the provided information.
   *
   * @param  messageID           The message ID for the LDAP message that is
   *                             associated with this LDAP result.
   * @param  passwordPolicyDN    The DN of the password policy that was used in
   *                             in the course of generating the password.  It
   *                             must not be {@code null}.
   * @param  generatedPasswords  The list of generated passwords.  It must not
   *                             be {@code null} or empty.
   * @param  controls            An optional set of controls for the response,
   *                             if any.  It may be {@code null} or empty if no
   *                             controls are needed.
   */
  public GeneratePasswordExtendedResult(final int messageID,
              @NotNull final String passwordPolicyDN,
              @NotNull final List<GeneratedPassword> generatedPasswords,
              @Nullable final Control... controls)
  {
    this(messageID, ResultCode.SUCCESS, null, null, null, passwordPolicyDN,
         generatedPasswords, controls);
  }



  /**
   * Creates a new generate password extended result with the provided
   * information.
   *
   * @param  messageID           The message ID for the LDAP message that is
   *                             associated with this LDAP result.
   * @param  resultCode          The result code for the response.  It must not
   *                             be {@code null}.
   * @param  diagnosticMessage   The diagnostic message for the response.  It
   *                             may be {@code null} if none is needed.
   * @param  matchedDN           The matched DN for the response.  It may be
   *                             {@code null} if none is needed.
   * @param  referralURLs        The set of referral URLs for the response.  It
   *                             may be {@code null} or empty if none are
   *                             needed.
   * @param  passwordPolicyDN    The DN of the password policy that was used in
   *                             in the course of generating the password.  It
   *                             must not be {@code null} for a successful
   *                             result, but must be {@code null} for a
   *                             non-successful result.
   * @param  generatedPasswords  The list of generated passwords.  It must not
   *                             be {@code null} or empty for a successful
   *                             result, but must be {@code null} or empty for a
   *                             non-successful result.
   * @param  controls            An optional set of controls for the response,
   *                             if any.  It may be {@code null} or empty if no
   *                             controls are needed.
   */
  public GeneratePasswordExtendedResult(final int messageID,
              @NotNull final ResultCode resultCode,
              @Nullable final String diagnosticMessage,
              @Nullable final String matchedDN,
              @Nullable final String[] referralURLs,
              @Nullable final String passwordPolicyDN,
              @Nullable final List<GeneratedPassword> generatedPasswords,
              @Nullable final Control... controls)
  {
    super(messageID, resultCode, diagnosticMessage, matchedDN, referralURLs,
         (resultCode == ResultCode.SUCCESS
              ? GENERATE_PASSWORD_RESULT_OID
              : null),
         (resultCode == ResultCode.SUCCESS
              ? encodeValue(passwordPolicyDN, generatedPasswords)
              : null),
         controls);

    this.passwordPolicyDN = passwordPolicyDN;

    if (resultCode == ResultCode.SUCCESS)
    {
      this.generatedPasswords = Collections.unmodifiableList(
           new ArrayList<>(generatedPasswords));
    }
    else
    {
      Validator.ensureTrue((passwordPolicyDN == null),
           "GeneratePasswordExtendedResult.passwordPolicyDN must be null for " +
                "a non-success result.");
      Validator.ensureTrue(
           ((generatedPasswords == null) || generatedPasswords.isEmpty()),
           "GeneratePasswordExtendedResult.generatedPasswords must be null " +
                "or empty for a non-success result.");

      this.generatedPasswords = Collections.emptyList();
    }
  }



  /**
   * Creates an ASN.1 octet string that is suitable for the value of a
   * successful generate password extended result.
   *
   * @param  passwordPolicyDN    The DN of the password policy that was used in
   *                             in the course of generating the password.  It
   *                             must not be {@code null}.
   * @param  generatedPasswords  The list of generated passwords.  It must not
   *                             be {@code null} or empty.
   *
   * @return  The ASN.1 octet string that was created.
   */
  @NotNull()
  private static ASN1OctetString encodeValue(
                      @NotNull final String passwordPolicyDN,
                      @NotNull final List<GeneratedPassword> generatedPasswords)
  {
    Validator.ensureNotNullOrEmpty(passwordPolicyDN,
         "GeneratePasswordExtendedResult.passwordPolicyDN must not be null " +
              "or empty in a success result.");
    Validator.ensureNotNullOrEmpty(generatedPasswords,
         "GeneratePasswordExtendedResult.generatedPasswords must not be null " +
              "or empty in a success result.");

    final List<ASN1Element> passwordElements =
         new ArrayList<>(generatedPasswords.size());
    for (final GeneratedPassword p : generatedPasswords)
    {
      passwordElements.add(p.encode());
    }

    final ASN1Sequence valueSequence = new ASN1Sequence(
         new ASN1OctetString(passwordPolicyDN),
         new ASN1Sequence(passwordElements));

    return new ASN1OctetString(valueSequence.encode());
  }



  /**
   * Creates a new generate password extended result from the provided extended
   * result.
   *
   * @param  extendedResult  The extended result to be decoded as a generate
   *                         password extended result.  It must not be
   *                         {@code null}.
   *
   * @throws  LDAPException  If the provided extended result cannot be decoded
   *                         as a generate password result.
   */
  public GeneratePasswordExtendedResult(
              @NotNull final ExtendedResult extendedResult)
         throws LDAPException
  {
    super(extendedResult);

    final ASN1OctetString value = extendedResult.getValue();
    if (value == null)
    {
      if (extendedResult.getResultCode() == ResultCode.SUCCESS)
      {
        throw new LDAPException(ResultCode.DECODING_ERROR,
             ERR_GENERATE_PASSWORD_RESULT_SUCCESS_MISSING_VALUE.get());
      }

      passwordPolicyDN = null;
      generatedPasswords = Collections.emptyList();
      return;
    }

    if (extendedResult.getResultCode() != ResultCode.SUCCESS)
    {
      throw new LDAPException(ResultCode.DECODING_ERROR,
           ERR_GENERATE_PASSWORD_RESULT_NON_SUCCESS_WITH_VALUE.get(
                String.valueOf(extendedResult.getResultCode())));
    }

    try
    {
      final ASN1Element[] valueElements =
           ASN1Sequence.decodeAsSequence(value.getValue()).elements();
      passwordPolicyDN =
           ASN1OctetString.decodeAsOctetString(valueElements[0]).stringValue();

      final ASN1Element[] pwElements =
           ASN1Sequence.decodeAsSequence(valueElements[1]).elements();
      final List<GeneratedPassword> pwList =
           new ArrayList<>(pwElements.length);
      for (final ASN1Element e : pwElements)
      {
        pwList.add(GeneratedPassword.decode(e));
      }

      if (pwList.isEmpty())
      {
        throw new LDAPException(ResultCode.DECODING_ERROR,
             ERR_GENERATE_PASSWORD_RESULT_DECODE_NO_PASSWORDS.get());
      }

      generatedPasswords = Collections.unmodifiableList(pwList);
    }
    catch (final LDAPException e)
    {
      Debug.debugException(e);
      throw e;
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.DECODING_ERROR,
           ERR_GENERATE_PASSWORD_RESULT_DECODING_ERROR.get(
                StaticUtils.getExceptionMessage(e)),
           e);
    }
  }



  /**
   * Retrieves the DN of the password policy that was used in the course of
   * generating and validating the passwords.
   *
   * @return  The DN of the password policy that was used in the course of
   *          generating and validating the passwords, or {@code null} if the
   *          operation was not processed successfully.
   */
  @Nullable()
  public String getPasswordPolicyDN()
  {
    return passwordPolicyDN;
  }



  /**
   * Retrieves the list of passwords that were generated by the server.
   *
   * @return  The list of passwords that were generated by the server, or an
   *          empty list if the operation was not processed successfully.
   */
  @NotNull()
  public List<GeneratedPassword> getGeneratedPasswords()
  {
    return generatedPasswords;
  }



  /**
   * {@inheritDoc}
   */
  @Override()
  @NotNull()
  public String getExtendedResultName()
  {
    return INFO_GENERATE_PASSWORD_RESULT_NAME.get();
  }



  /**
   * Appends a string representation of this extended result to the provided
   * buffer.
   *
   * @param  buffer  The buffer to which a string representation of this
   *                 extended result will be appended.
   */
  @Override()
  public void toString(@NotNull final StringBuilder buffer)
  {
    buffer.append("GeneratePasswordExtendedResult(resultCode=");
    buffer.append(getResultCode());

    final int messageID = getMessageID();
    if (messageID >= 0)
    {
      buffer.append(", messageID=");
      buffer.append(messageID);
    }

    final String diagnosticMessage = getDiagnosticMessage();
    if (diagnosticMessage != null)
    {
      buffer.append(", diagnosticMessage='");
      buffer.append(diagnosticMessage);
      buffer.append('\'');
    }

    final String matchedDN = getMatchedDN();
    if (matchedDN != null)
    {
      buffer.append(", matchedDN='");
      buffer.append(matchedDN);
      buffer.append('\'');
    }

    final String[] referralURLs = getReferralURLs();
    if (referralURLs.length > 0)
    {
      buffer.append(", referralURLs={");
      for (int i=0; i < referralURLs.length; i++)
      {
        if (i > 0)
        {
          buffer.append(", ");
        }

        buffer.append('\'');
        buffer.append(referralURLs[i]);
        buffer.append('\'');
      }
      buffer.append('}');
    }

    if (passwordPolicyDN != null)
    {
      buffer.append(", passwordPolicyDN='");
      buffer.append(passwordPolicyDN);
      buffer.append('\'');
    }

    if (! generatedPasswords.isEmpty())
    {
      buffer.append(", generatedPasswords={ ");

      final Iterator<GeneratedPassword> iterator =
           generatedPasswords.iterator();
      while (iterator.hasNext())
      {
        iterator.next().toString(buffer);

        if (iterator.hasNext())
        {
          buffer.append(", ");
        }
      }

      buffer.append(" }");
    }

    final Control[] responseControls = getResponseControls();
    if (responseControls.length > 0)
    {
      buffer.append(", responseControls={");
      for (int i=0; i < responseControls.length; i++)
      {
        if (i > 0)
        {
          buffer.append(", ");
        }

        buffer.append(responseControls[i]);
      }
      buffer.append('}');
    }

    buffer.append(')');
  }
}
